#!/usr/bin/python
# encoding: utf-8
import os, sys
from contextlib import asynccontextmanager
from datetime import date
import inspect
import json
from typing import List
from fastapi.openapi.docs import get_swagger_ui_html
import fastapi_jsonrpc as jsonrpc
from fastapi import Header, applications, Request
from logger import myLogger
import datetime
import numpy as np
import pandas as pd
import math
import ast

from iotdb.Session import Session
from iotdb.template.MeasurementNode import MeasurementNode
from iotdb.template.Template import Template
from iotdb.utils.BitMap import BitMap
from iotdb.utils.IoTDBConstants import TSDataType, TSEncoding, Compressor
from iotdb.utils.Tablet import Tablet
from iotdb.utils.NumpyTablet import NumpyTablet
#from peewee import fn, JOIN, chunked
from configparser import ConfigParser
import requests

import atexit

import sqlite3

STORAGE_PATH = 'RKSimuvppplt01'

logger = myLogger('api', 'trace.log')

def swagger_url_patch(*args, **kwargs):
    return get_swagger_ui_html(
        *args, **kwargs,
        swagger_js_url='https://cdn.bootcdn.net/ajax/libs/swagger-ui/5.6.2/swagger-ui-bundle.js',
        swagger_css_url='https://cdn.bootcdn.net/ajax/libs/swagger-ui/5.6.2/swagger-ui.css'
    )


applications.get_swagger_ui_html = swagger_url_patch

'''
def get_permission_by_token(auth_token) -> List[str]:
    logger.info(f'token: {auth_token}')
    return list()
'''

# errors

class AuthError(jsonrpc.BaseError):
    CODE = 7000
    MESSAGE = 'Auth error'


class AccountNotFound(jsonrpc.BaseError):
    CODE = 6000
    MESSAGE = 'Account not found'


class NotPermitted(jsonrpc.BaseError):
    CODE = 6001
    MESSAGE = 'Not Permitted'


class MyError(jsonrpc.BaseError):
    CODE = -1
    MESSAGE = ''

    def __init__(self, data=None, code: int = None, msg: str = None):
        if code:
            self.CODE = code
        if msg:
            self.MESSAGE = msg
        super().__init__(data)

# dependencies

'''
def get_auth_user(
    # this will become the header-parameter of json-rpc method that uses this dependency
    jwt: str = Header(None, title='jwt',
                      alias='Authorization', pattern='Bearer .*'),
) -> bool:
    if not jwt:
        raise AuthError

    try:
        return get_permission_by_token(jwt)
    except KeyError:
        raise AuthError
'''

#------------IoTDB database connection ----------------

def iotdbconnect():

    ip = "127.0.0.1"
    port_ = "6667"
    username_ = "root"
    password_ = "root"
    session = Session(ip, port_, username_, password_)
    session.open(False)
    return session

#------------ sqlite configuration db connection---------
def sqliteconnect():
    sqlconn = sqlite3.connect('idvmeterinfo.db', check_same_thread=False)
    return sqlconn

# JSON-RPC middlewares
@asynccontextmanager
async def logging_middleware(ctx: jsonrpc.JsonRpcContext):
    print(ctx.http_request.headers)
    logger.info('Request: %r', ctx.raw_request)
    try:
        yield
    finally:
        logger.info('Response: %r', ctx.raw_response)


# JSON-RPC entrypoint
common_errors = [AccountNotFound, AuthError, NotPermitted]
common_errors.extend(jsonrpc.Entrypoint.default_errors)

api = jsonrpc.Entrypoint(
    # Swagger shows for entrypoint common parameters gathered by dependencies and common_dependencies:
    #    - json-rpc-parameter 'account_id'
    #    - header parameter 'user-auth-token'
    '/',
    name='IoTDB Data Base',
    errors=common_errors,
    middlewares=[logging_middleware],
    # this dependencies called once for whole json-rpc batch request
    # dependencies=[Depends(get_auth_user)],
    # this dependencies called separately for every json-rpc request in batch request
    # common_dependencies=[Depends(get_account)],
)

# timePoint format: '2024-01-19 13:00'
def basicGetOneTimePointQuery_1(devid:str, timePoint:str, dataType:str='P', storageGroupPath:str = STORAGE_PATH):
    try:
        session = iotdbconnect()
    except:
        raise MyError(msg='Could not connect to the IoTDB database')
    else:

        qureystr = 'select last `%s`.%s from root.%s.** where time <= %s:00'%(devid, dataType, storageGroupPath, timePoint)
        res2 = session.execute_query_statement(qureystr)
        df2 = res2.todf()
        if df2.shape[0] == 0:
            raise MyError(msg='Could not find the %s value for device %s at time %s'%(dataType, devid, timePoint))

        return float(df2.iloc[0, 2])
    finally:
        session.close()

def basicGetOneTimePeriodQuery(devid:str, starttime:str, endtime:str, accuracy:str= 'k5', dataType:str='P', storageGroupPath:str=STORAGE_PATH):
    try:
        session = iotdbconnect()
    except:
        raise MyError(msg='Could not connect to the IoTDB database')
    else:

        if accuracy[0] == 'k':
            queryTimeInterval = '%sm'%(accuracy[1:])
        elif accuracy == 'day':
            queryTimeInterval = '1d'
        elif accuracy == 'month':
            queryTimeInterval = '1mo'
        else:
            raise MyError( msg='The accuracy %s is not defined correctly'%(accuracy) )

        qureystr = 'select first_value(`%s`.%s) from root.%s.** group by ([%s, %s), %s)' % (devid, dataType, storageGroupPath,
                                                                                    starttime, endtime,
                                                                                    queryTimeInterval)
        res2 = session.execute_query_statement(qureystr)
        df2 = res2.todf()

        if df2.empty: # if it is empty, directly return the df2
            '''
            qureystr = 'select avg(`%s`.%s) from %s.** group by ([%s, %s), %s) fill(0.0)' % (devid, dataType, storageGroupPath,
                                                                                     starttime, endtime,
                                                                                     queryTimeInterval)
            res2 = session.execute_query_statement(qureystr)
            df2 = res2.todf()
            '''
            return df2

        '''
        for idx in range(df2.shape[0]):

            if np.isnan(df2.iloc[idx, 1]):
                df2.iloc[idx, 1] = 0.0
        '''
        nonanidxlst = []
        nonanvaluelst = []
        fullidxlst = []
        for idx in range(df2.shape[0]):
            '''
            df2.iloc[idx, 0] = datetime.datetime.strftime(datetime.datetime.fromtimestamp(df2.iloc[idx, 0] / 1000),
                                                        '%Y-%m-%d %H:%M:%S')
            '''
            fullidxlst.append(idx)
            if not np.isnan(df2.iloc[idx, 1]):
                nonanidxlst.append(idx)
                nonanvaluelst.append(df2.iloc[idx, 1])

        #---------do interpolation----------------
        fullvalues = np.interp(fullidxlst, nonanidxlst, nonanvaluelst)
        for idx in range(df2.shape[0]):
            df2.iloc[idx, 1] = fullvalues[idx]

        return df2

    finally:
        session.close()

def basicGetOneTimePeriodQueryFillZero(devid:str, starttime:str, endtime:str, accuracy:str= 'k5', dataType:str='P', storageGroupPath:str=STORAGE_PATH):
    try:
        session = iotdbconnect()
    except:
        raise MyError(msg='Could not connect to the IoTDB database')
    else:

        if accuracy[0] == 'k':
            queryTimeInterval = '%sm'%(accuracy[1:])
        elif accuracy == 'day':
            queryTimeInterval = '1d'
        elif accuracy == 'month':
            queryTimeInterval = '1mo'
        else:
            raise MyError( msg='The accuracy %s is not defined correctly'%(accuracy) )

        qureystr = 'select avg(`%s`.%s) from root.%s.** group by ([%s, %s), %s) fill(0.0)' % (devid, dataType,storageGroupPath,
                                                                                    starttime, endtime,
                                                                                    queryTimeInterval)
        res2 = session.execute_query_statement(qureystr)
        df2 = res2.todf()

        '''
        for idx in range(df2.shape[0]):

            if np.isnan(df2.iloc[idx, 1]):
                df2.iloc[idx, 1] = 0.0
        '''
        nonanidxlst = []
        nonanvaluelst = []
        fullidxlst = []
        for idx in range(df2.shape[0]):
            '''
            df2.iloc[idx, 0] = datetime.datetime.strftime(datetime.datetime.fromtimestamp(df2.iloc[idx, 0] / 1000),
                                                        '%Y-%m-%d %H:%M:%S')
            '''
            fullidxlst.append(idx)
            if not np.isnan(df2.iloc[idx, 1]):
                nonanidxlst.append(idx)
                nonanvaluelst.append(df2.iloc[idx, 1])

        #---------do interpolation----------------
        fullvalues = np.interp(fullidxlst, nonanidxlst, nonanvaluelst)
        for idx in range(df2.shape[0]):
            df2.iloc[idx, 1] = fullvalues[idx]

        return df2

    finally:
        session.close()

def basicGetOneTimePeriodQuery_interpolation(devid:str, starttime:str, endtime:str, accuracy:str= 'k5',
                                             dataType:str='P', storageGroupPath:str=STORAGE_PATH):
    try:
        session = iotdbconnect()
    except:
        raise MyError(msg='Could not connect to the IoTDB database')
    else:

        if accuracy[0] == 'k':
            queryTimeInterval = '%sm'%(accuracy[1:])
        elif accuracy == 'day':
            queryTimeInterval = '1d'
        elif accuracy == 'month':
            queryTimeInterval = '1mo'
        else:
            raise MyError( msg='The accuracy %s is not defined correctly'%(accuracy) )

        qureystr = 'select avg(`%s`.%s) from root.%s.** group by ([%s, %s), %s)' % (devid, dataType,storageGroupPath,
                                                                                    starttime, endtime,
                                                                                    queryTimeInterval)
        res2 = session.execute_query_statement(qureystr)
        df2 = res2.todf()

        nonanidxlst = []
        nonanvaluelst = []
        fullidxlst = []
        firstnoNANvalue = False
        innotnancount = 0
        for idx in range(df2.shape[0]):
            '''
            df2.iloc[idx, 0] = datetime.datetime.strftime(datetime.datetime.fromtimestamp(df2.iloc[idx, 0] / 1000),
                                                        '%Y-%m-%d %H:%M:%S')
            '''
            fullidxlst.append(idx)
            if not np.isnan(df2.iloc[idx, 1]):
                nonanidxlst.append(idx)
                nonanvaluelst.append(df2.iloc[idx, 1])
                if innotnancount == 0:
                    firstnoNANvalue = True
                    innotnancount += 1
            else:
                if not firstnoNANvalue:
                    nonanidxlst.append(idx)
                    nonanvaluelst.append(0.0)

        #---------do interpolation----------------
        fullvalues = np.interp(fullidxlst, nonanidxlst, nonanvaluelst)
        for idx in range(df2.shape[0]):
            df2.iloc[idx, 1] = fullvalues[idx]

        return df2

    finally:
        session.close()

def basicGetOneTimePeriodQuerywithSlidingWindow(devid:str, starttime:str, endtime:str, accuracy:str= 'k5',
                                                slidingwindow:str='k5',
                                                dataType:str='P', storageGroupPath:str=STORAGE_PATH):
    try:
        session = iotdbconnect()
    except:
        raise MyError(msg='Could not connect to the IoTDB database')
    else:

        if accuracy[0] == 'k':
            queryTimeInterval = '%sm'%(accuracy[1:])
        elif accuracy == 'day':
            queryTimeInterval = '1d'
        elif accuracy == 'month':
            queryTimeInterval = '1mo'
        else:
            raise MyError( msg='The accuracy %s is not defined correctly'%(accuracy) )

        if slidingwindow[0] == 'k':
            querySlidingWindow = '%sm'%(slidingwindow[1:])
        elif slidingwindow == 'day':
            querySlidingWindow = '1d'
        elif slidingwindow == 'month':
            querySlidingWindow = '1mo'
        else:
            raise MyError( msg='The slidingwindow %s is not defined correctly'%(slidingwindow) )

        qureystr = 'select avg(`%s`.%s) from root.%s.** group by ([%s, %s), %s, %s)' % (devid, dataType,storageGroupPath,
                                                                                    starttime, endtime,
                                                                                    querySlidingWindow,
                                                                                    queryTimeInterval)

        res2 = session.execute_query_statement(qureystr)
        df2 = res2.todf()

        '''
                for idx in range(df2.shape[0]):

                    if np.isnan(df2.iloc[idx, 1]):
                        df2.iloc[idx, 1] = 0.0
                '''
        nonanidxlst = []
        nonanvaluelst = []
        fullidxlst = []
        for idx in range(df2.shape[0]):
            '''
            df2.iloc[idx, 0] = datetime.datetime.strftime(datetime.datetime.fromtimestamp(df2.iloc[idx, 0] / 1000),
                                                        '%Y-%m-%d %H:%M:%S')
            '''
            fullidxlst.append(idx)
            if not np.isnan(df2.iloc[idx, 1]):
                nonanidxlst.append(idx)
                nonanvaluelst.append(df2.iloc[idx, 1])

        # ---------do interpolation----------------
        fullvalues = np.interp(fullidxlst, nonanidxlst, nonanvaluelst)
        for idx in range(df2.shape[0]):
            df2.iloc[idx, 1] = fullvalues[idx]

        return df2

    finally:
        session.close()

def basicGetOneTimePeriodQuerywithSlidingWindow_interpolation(devid:str, starttime:str, endtime:str, accuracy:str= 'k5',
                                                              slidingwindow:str='k5', dataType:str='P',
                                                              storageGroupPath:str=STORAGE_PATH):
    try:
        session = iotdbconnect()
    except:
        raise MyError(msg='Could not connect to the IoTDB database')
    else:

        if accuracy[0] == 'k':
            queryTimeInterval = '%sm'%(accuracy[1:])
        elif accuracy == 'day':
            queryTimeInterval = '1d'
        elif accuracy == 'month':
            queryTimeInterval = '1mo'
        else:
            raise MyError( msg='The accuracy %s is not defined correctly'%(accuracy) )

        if slidingwindow[0] == 'k':
            querySlidingWindow = '%sm'%(slidingwindow[1:])
        elif slidingwindow == 'day':
            querySlidingWindow = '1d'
        elif slidingwindow == 'month':
            querySlidingWindow = '1mo'
        else:
            raise MyError( msg='The slidingwindow %s is not defined correctly'%(slidingwindow) )

        qureystr = 'select avg(`%s`.%s) from root.%s.** group by ([%s, %s), %s, %s)' % (devid, dataType,storageGroupPath,
                                                                                    starttime, endtime,
                                                                                    querySlidingWindow,
                                                                                    queryTimeInterval)

        res2 = session.execute_query_statement(qureystr)
        df2 = res2.todf()

        nonanidxlst = []
        nonanvaluelst = []
        fullidxlst = []
        firstnoNANvalue = False
        innotnancount = 0
        for idx in range(df2.shape[0]):
            '''
            df2.iloc[idx, 0] = datetime.datetime.strftime(datetime.datetime.fromtimestamp(df2.iloc[idx, 0] / 1000),
                                                        '%Y-%m-%d %H:%M:%S')
            '''
            fullidxlst.append(idx)
            if not np.isnan(df2.iloc[idx, 1]):
                nonanidxlst.append(idx)
                nonanvaluelst.append(df2.iloc[idx, 1])
                if innotnancount == 0:
                    firstnoNANvalue = True
                    innotnancount += 1
            else:
                if not firstnoNANvalue:
                    nonanidxlst.append(idx)
                    nonanvaluelst.append(0.0)

        #---------do interpolation----------------
        fullvalues = np.interp(fullidxlst, nonanidxlst, nonanvaluelst)
        for idx in range(df2.shape[0]):
            df2.iloc[idx, 1] = fullvalues[idx]

        return df2

    finally:
        session.close()

# JSON-RPC methods of this entrypoint

# timePoint format: '2024-01-19 13:00'
@api.method()
def GetOneTimePointQuery(devid:str, time:str, dataType:str='P')-> List[dict]:
    tmp = basicGetOneTimePointQuery_1(devid, time, dataType)
    return [{'time': '%s:00'%(time), 'value': '%.2f'%(tmp)}]

@api.method()
def GetOneTimePeriodQuery(devid:str, starttime:str, endtime:str, accuracy:str= 'k5', dataType:str='P')-> List[dict]:
    dftmp = basicGetOneTimePeriodQuery(devid, starttime, endtime, accuracy, dataType)
    return [{'value': '%.2f'%(irow[1]), 'time': datetime.datetime.fromtimestamp(irow[0] / 1000).strftime("%Y-%m-%d %H:%M:%S")}
            for idx, irow in dftmp.iterrows()]

@api.method()
def setCtrlMode(mode: str) -> List[dict]:
    json_file_path = 'DERParams.json'

    # 从JSON文件读取数据
    with open(json_file_path, 'r') as json_file:
        DERCtrl = json.load(json_file)  #
        modeIndx = int(mode)
        if modeIndx == 1 or modeIndx == 2 or modeIndx == 3: # 调节模式：1 ：AC+EV 2：AC 3：EV
            DERCtrl['Mode'] = modeIndx
            with open(json_file_path, 'w') as json_file:
                json.dump(DERCtrl, json_file, indent=4)

            return [{'success': '0'}]
        else:
            return [{'success': 'Fail, the mode set value %d is not a correct mode value'%(modeIndx)}]

@api.method()
def setCtrlACTemp(ACMax: str, ACMin: str) -> List[dict]:
    json_file_path = 'DERParams.json'

    # 从JSON文件读取数据
    with open(json_file_path, 'r') as json_file:
        DERCtrl = json.load(json_file)  #
        acMaxTemp = float(ACMax)
        acMinTemp = float(ACMin)
        if acMaxTemp > acMinTemp: # 调节模式：1 ：AC+EV 2：AC 3：EV
            DERCtrl['AC_Tmax'] = acMaxTemp
            DERCtrl['AC_Tmin'] = acMinTemp

            with open(json_file_path, 'w') as json_file:
                json.dump(DERCtrl, json_file, indent=4)

            return [{'success': '0'}]
        else:
            return [{'success': 'Fail, the ACMax %s and ACMin %s is not set correct'%(ACMax, ACMin)}]

'''
setScene: 1, 2, 3, for 午高峰、晚高峰、双峰
setCtrlMode: 0, 1, 2, 3, 4 for no ctrol, AC+EV control, AC control only, EV control only, real-time control
'''
@api.method()
def getVirtualCapSimuData(setScene:str, setCtrlMode:str, accuracy:str= 'k15')-> List[dict]:
    starttime = '2024-10-18 00:00:00'
    endtime = '2024-10-18 23:15:00'

    sceneIdx = int(setScene)
    ctrlMode = int(setCtrlMode)

    if sceneIdx >= 4 or sceneIdx <= 0:
        raise MyError(msg='the setScene %s is not correct!'%(setScene))

    if ctrlMode >= 5 or sceneIdx <= -1:
        raise MyError(msg='the setCtrlMode %s is not correct!' % (setCtrlMode))

    #---------get area total load, ini and opt-----------------------------------------------------
    areaStr = 'Area_total'
    devid = 'LD%d_total@%s'%(sceneIdx, areaStr)

    dataType = 'P_ini'
    df_total_pini = basicGetOneTimePeriodQuery(devid, starttime, endtime, accuracy, dataType)
    if ctrlMode == 0:
        dataType = 'P_ini'
    else:
        dataType = 'P_opt%d'%(ctrlMode)
    df_total_popt = basicGetOneTimePeriodQuery(devid, starttime, endtime, accuracy, dataType)

    # ---------get zone 1 total load, ini and opt-----------------------------------------------------
    areaStr = 'Area_001'
    devid = 'LD%d_total@%s' % (sceneIdx, areaStr)

    dataType = 'P_ini'
    df_zone1_pini = basicGetOneTimePeriodQuery(devid, starttime, endtime, accuracy, dataType)
    if ctrlMode == 0:
        dataType = 'P_ini'
    else:
        dataType = 'P_opt%d' % (ctrlMode)
    df_zone1_popt = basicGetOneTimePeriodQuery(devid, starttime, endtime, accuracy, dataType)

    # ---------get zone 2 total load, ini and opt-----------------------------------------------------
    areaStr = 'Area_002'
    devid = 'LD%d_total@%s' % (sceneIdx, areaStr)

    dataType = 'P_ini'
    df_zone2_pini = basicGetOneTimePeriodQuery(devid, starttime, endtime, accuracy, dataType)
    if ctrlMode == 0:
        dataType = 'P_ini'
    else:
        dataType = 'P_opt%d' % (ctrlMode)
    df_zone2_popt = basicGetOneTimePeriodQuery(devid, starttime, endtime, accuracy, dataType)

    # ---------get zone 3 total load, ini and opt-----------------------------------------------------
    areaStr = 'Area_003'
    devid = 'LD%d_total@%s' % (sceneIdx, areaStr)

    dataType = 'P_ini'
    df_zone3_pini = basicGetOneTimePeriodQuery(devid, starttime, endtime, accuracy, dataType)
    if ctrlMode == 0:
        dataType = 'P_ini'
    else:
        dataType = 'P_opt%d' % (ctrlMode)
    df_zone3_popt = basicGetOneTimePeriodQuery(devid, starttime, endtime, accuracy, dataType)

    #-------------------compute the capacity reduce--------------------------------------------------
    MVARATE = 2000.0
    increasePerc = (max(df_total_pini.iloc[:, 1]) - max(df_total_popt.iloc[:, 1])) / MVARATE * 100.0

    outputList = []
    outputList.append ({'increasePerc': '%.2f'%(increasePerc)})
    for idx, irow in df_total_pini.iterrows():
        dicttmp = {}
        dicttmp['time'] = datetime.datetime.fromtimestamp(irow[0] / 1000).strftime("%H:%M")
        dicttmp['total_P_ini'] = '%.2f'%(df_total_pini.iloc[idx, 1])
        dicttmp['total_P_opt'] = '%.2f' % (df_total_popt.iloc[idx, 1])

        dicttmp['zone1_P_ini'] = '%.2f' % (df_zone1_pini.iloc[idx, 1])
        dicttmp['zone1_P_opt'] = '%.2f' % (df_zone1_popt.iloc[idx, 1])

        dicttmp['zone2_P_ini'] = '%.2f' % (df_zone2_pini.iloc[idx, 1])
        dicttmp['zone2_P_opt'] = '%.2f' % (df_zone2_popt.iloc[idx, 1])

        dicttmp['zone3_P_ini'] = '%.2f' % (df_zone3_pini.iloc[idx, 1])
        dicttmp['zone3_P_opt'] = '%.2f' % (df_zone3_popt.iloc[idx, 1])

        outputList.append(dicttmp)

    return outputList

#-----------------start the app-----------------------------------------------------------
app = jsonrpc.API()
app.bind_entrypoint(api)

#atexit.register(ExecuteBeforeExit)

if __name__ == '__main__':
    import uvicorn
    import argparse
    parser = argparse.ArgumentParser()
    parser.add_argument('--port', '-p', type=int, default=8019)  # app port
    args = parser.parse_args()
    params = vars(args)
    uvicorn.run(app='jiadingAPI:app', port=params['port'],  reload=True)